/*
 * $Header:
 * /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons
 * //httpclient/src/java/org/apache/commons/httpclient/Cookie.java,v 1.44
 * 2004/06/05 16:49:20 olegk Exp $ $Revision: 531354 $ $Date: 2007-04-23
 * 08:53:20 +0200 (Mon, 23 Apr 2007) $
 *
 * ====================================================================
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many individuals on
 * behalf of the Apache Software Foundation. For more information on the Apache
 * Software Foundation, please see <http://www.apache.org/>.
 */

package com.microsoft.tfs.core.httpclient;

import java.io.Serializable;
import java.util.Comparator;
import java.util.Date;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.microsoft.tfs.core.httpclient.cookie.CookiePolicy;
import com.microsoft.tfs.core.httpclient.cookie.CookieSpec;
import com.microsoft.tfs.core.httpclient.util.LangUtils;

/**
 * <p>
 * HTTP "magic-cookie" represents a piece of state information that the HTTP
 * agent and the target server can exchange to maintain a session.
 * </p>
 *
 * @author B.C. Holmes
 * @author <a href="mailto:jericho@thinkfree.com">Park, Sung-Gu</a>
 * @author <a href="mailto:dsale@us.britannica.com">Doug Sale</a>
 * @author Rod Waldhoff
 * @author dIon Gillard
 * @author Sean C. Sullivan
 * @author <a href="mailto:JEvans@Cyveillance.com">John Evans</a>
 * @author Marc A. Saegesser
 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
 *
 * @version $Revision: 531354 $ $Date: 2007-04-23 08:53:20 +0200 (Mon, 23 Apr
 *          2007) $
 */
public class Cookie extends NameValuePair implements Serializable, Comparator<Cookie> {
    private static final long serialVersionUID = 4469080599899521170L;

    // ----------------------------------------------------------- Constructors

    /**
     * Default constructor. Creates a blank cookie
     */

    public Cookie() {
        this(null, "noname", null, null, null, false);
    }

    /**
     * Creates a cookie with the given name, value and domain attribute.
     *
     * @param name
     *        the cookie name
     * @param value
     *        the cookie value
     * @param domain
     *        the domain this cookie can be sent to
     */
    public Cookie(final String domain, final String name, final String value) {
        this(domain, name, value, null, null, false);
    }

    /**
     * Creates a cookie with the given name, value, domain attribute, path
     * attribute, expiration attribute, and secure attribute
     *
     * @param name
     *        the cookie name
     * @param value
     *        the cookie value
     * @param domain
     *        the domain this cookie can be sent to
     * @param path
     *        the path prefix for which this cookie can be sent
     * @param expires
     *        the {@link Date} at which this cookie expires, or <tt>null</tt> if
     *        the cookie expires at the end of the session
     * @param secure
     *        if true this cookie can only be sent over secure connections
     * @throws IllegalArgumentException
     *         If cookie name is null or blank, cookie name contains a blank, or
     *         cookie name starts with character $
     *
     */
    public Cookie(
        final String domain,
        final String name,
        final String value,
        final String path,
        final Date expires,
        final boolean secure) {

        super(name, value);
        LOG.trace("enter Cookie(String, String, String, String, Date, boolean)");
        if (name == null) {
            throw new IllegalArgumentException("Cookie name may not be null");
        }
        if (name.trim().equals("")) {
            throw new IllegalArgumentException("Cookie name may not be blank");
        }
        setPath(path);
        setDomain(domain);
        setExpiryDate(expires);
        setSecure(secure);
    }

    /**
     * Creates a cookie with the given name, value, domain attribute, path
     * attribute, maximum age attribute, and secure attribute
     *
     * @param name
     *        the cookie name
     * @param value
     *        the cookie value
     * @param domain
     *        the domain this cookie can be sent to
     * @param path
     *        the path prefix for which this cookie can be sent
     * @param maxAge
     *        the number of seconds for which this cookie is valid. maxAge is
     *        expected to be a non-negative number. <tt>-1</tt> signifies that
     *        the cookie should never expire.
     * @param secure
     *        if <tt>true</tt> this cookie can only be sent over secure
     *        connections
     */
    public Cookie(
        final String domain,
        final String name,
        final String value,
        final String path,
        final int maxAge,
        final boolean secure) {

        this(domain, name, value, path, null, secure);
        if (maxAge < -1) {
            throw new IllegalArgumentException("Invalid max age:  " + Integer.toString(maxAge));
        }
        if (maxAge >= 0) {
            setExpiryDate(new Date(System.currentTimeMillis() + maxAge * 1000L));
        }
    }

    /**
     * Returns the comment describing the purpose of this cookie, or
     * <tt>null</tt> if no such comment has been defined.
     *
     * @return comment
     *
     * @see #setComment(String)
     */
    public String getComment() {
        return cookieComment;
    }

    /**
     * If a user agent (web browser) presents this cookie to a user, the
     * cookie's purpose will be described using this comment.
     *
     * @param comment
     *
     * @see #getComment()
     */
    public void setComment(final String comment) {
        cookieComment = comment;
    }

    /**
     * Returns the expiration {@link Date} of the cookie, or <tt>null</tt> if
     * none exists.
     * <p>
     * <strong>Note:</strong> the object returned by this method is considered
     * immutable. Changing it (e.g. using setTime()) could result in undefined
     * behaviour. Do so at your peril.
     * </p>
     *
     * @return Expiration {@link Date}, or <tt>null</tt>.
     *
     * @see #setExpiryDate(java.util.Date)
     *
     */
    public Date getExpiryDate() {
        return cookieExpiryDate;
    }

    /**
     * Sets expiration date.
     * <p>
     * <strong>Note:</strong> the object returned by this method is considered
     * immutable. Changing it (e.g. using setTime()) could result in undefined
     * behaviour. Do so at your peril.
     * </p>
     *
     * @param expiryDate
     *        the {@link Date} after which this cookie is no longer valid.
     *
     * @see #getExpiryDate
     *
     */
    public void setExpiryDate(final Date expiryDate) {
        cookieExpiryDate = expiryDate;
    }

    /**
     * Returns <tt>false</tt> if the cookie should be discarded at the end of
     * the "session"; <tt>true</tt> otherwise.
     *
     * @return <tt>false</tt> if the cookie should be discarded at the end of
     *         the "session"; <tt>true</tt> otherwise
     */
    public boolean isPersistent() {
        return (null != cookieExpiryDate);
    }

    /**
     * Returns domain attribute of the cookie.
     *
     * @return the value of the domain attribute
     *
     * @see #setDomain(java.lang.String)
     */
    public String getDomain() {
        return cookieDomain;
    }

    /**
     * Sets the domain attribute.
     *
     * @param domain
     *        The value of the domain attribute
     *
     * @see #getDomain
     */
    public void setDomain(String domain) {
        if (domain != null) {
            final int ndx = domain.indexOf(":");
            if (ndx != -1) {
                domain = domain.substring(0, ndx);
            }
            cookieDomain = domain.toLowerCase();
        }
    }

    /**
     * Returns the path attribute of the cookie
     *
     * @return The value of the path attribute.
     *
     * @see #setPath(java.lang.String)
     */
    public String getPath() {
        return cookiePath;
    }

    /**
     * Sets the path attribute.
     *
     * @param path
     *        The value of the path attribute
     *
     * @see #getPath
     *
     */
    public void setPath(final String path) {
        cookiePath = path;
    }

    /**
     * @return <code>true</code> if this cookie should only be sent over secure
     *         connections.
     * @see #setSecure(boolean)
     */
    public boolean getSecure() {
        return isSecure;
    }

    /**
     * Sets the secure attribute of the cookie.
     * <p>
     * When <tt>true</tt> the cookie should only be sent using a secure protocol
     * (https). This should only be set when the cookie's originating server
     * used a secure protocol to set the cookie's value.
     *
     * @param secure
     *        The value of the secure attribute
     *
     * @see #getSecure()
     */
    public void setSecure(final boolean secure) {
        isSecure = secure;
    }

    /**
     * Returns the version of the cookie specification to which this cookie
     * conforms.
     *
     * @return the version of the cookie.
     *
     * @see #setVersion(int)
     *
     */
    public int getVersion() {
        return cookieVersion;
    }

    /**
     * Sets the version of the cookie specification to which this cookie
     * conforms.
     *
     * @param version
     *        the version of the cookie.
     *
     * @see #getVersion
     */
    public void setVersion(final int version) {
        cookieVersion = version;
    }

    /**
     * Returns true if this cookie has expired.
     *
     * @return <tt>true</tt> if the cookie has expired.
     */
    public boolean isExpired() {
        return (cookieExpiryDate != null && cookieExpiryDate.getTime() <= System.currentTimeMillis());
    }

    /**
     * Returns true if this cookie has expired according to the time passed in.
     *
     * @param now
     *        The current time.
     *
     * @return <tt>true</tt> if the cookie expired.
     */
    public boolean isExpired(final Date now) {
        return (cookieExpiryDate != null && cookieExpiryDate.getTime() <= now.getTime());
    }

    /**
     * Indicates whether the cookie had a path specified in a path attribute of
     * the <tt>Set-Cookie</tt> header. This value is important for generating
     * the <tt>Cookie</tt> header because some cookie specifications require
     * that the <tt>Cookie</tt> header should only include a path attribute if
     * the cookie's path was specified in the <tt>Set-Cookie</tt> header.
     *
     * @param value
     *        <tt>true</tt> if the cookie's path was explicitly set,
     *        <tt>false</tt> otherwise.
     *
     * @see #isPathAttributeSpecified
     */
    public void setPathAttributeSpecified(final boolean value) {
        hasPathAttribute = value;
    }

    /**
     * Returns <tt>true</tt> if cookie's path was set via a path attribute in
     * the <tt>Set-Cookie</tt> header.
     *
     * @return value <tt>true</tt> if the cookie's path was explicitly set,
     *         <tt>false</tt> otherwise.
     *
     * @see #setPathAttributeSpecified
     */
    public boolean isPathAttributeSpecified() {
        return hasPathAttribute;
    }

    /**
     * Indicates whether the cookie had a domain specified in a domain attribute
     * of the <tt>Set-Cookie</tt> header. This value is important for generating
     * the <tt>Cookie</tt> header because some cookie specifications require
     * that the <tt>Cookie</tt> header should only include a domain attribute if
     * the cookie's domain was specified in the <tt>Set-Cookie</tt> header.
     *
     * @param value
     *        <tt>true</tt> if the cookie's domain was explicitly set,
     *        <tt>false</tt> otherwise.
     *
     * @see #isDomainAttributeSpecified
     */
    public void setDomainAttributeSpecified(final boolean value) {
        hasDomainAttribute = value;
    }

    /**
     * Returns <tt>true</tt> if cookie's domain was set via a domain attribute
     * in the <tt>Set-Cookie</tt> header.
     *
     * @return value <tt>true</tt> if the cookie's domain was explicitly set,
     *         <tt>false</tt> otherwise.
     *
     * @see #setDomainAttributeSpecified
     */
    public boolean isDomainAttributeSpecified() {
        return hasDomainAttribute;
    }

    /**
     * Returns a hash code in keeping with the {@link Object#hashCode} general
     * hashCode contract.
     *
     * @return A hash code
     */
    @Override
    public int hashCode() {
        int hash = LangUtils.HASH_SEED;
        hash = LangUtils.hashCode(hash, getName());
        hash = LangUtils.hashCode(hash, cookieDomain);
        hash = LangUtils.hashCode(hash, cookiePath);
        return hash;
    }

    /**
     * Two cookies are equal if the name, path and domain match.
     *
     * @param obj
     *        The object to compare against.
     * @return true if the two objects are equal.
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (this == obj) {
            return true;
        }
        if (obj instanceof Cookie) {
            final Cookie that = (Cookie) obj;
            return LangUtils.equals(getName(), that.getName())
                && LangUtils.equals(cookieDomain, that.cookieDomain)
                && LangUtils.equals(cookiePath, that.cookiePath);
        } else {
            return false;
        }
    }

    /**
     * Return a textual representation of the cookie.
     *
     * @return string.
     */
    public String toExternalForm() {
        CookieSpec spec = null;
        if (getVersion() > 0) {
            spec = CookiePolicy.getCookieSpec(CookiePolicy.DEFAULT);
        } else {
            spec = CookiePolicy.getCookieSpec(CookiePolicy.NETSCAPE);
        }
        return spec.formatCookie(this);
    }

    /**
     * <p>
     * Compares two cookies to determine order for cookie header.
     * </p>
     * <p>
     * Most specific should be first.
     * </p>
     * <p>
     * This method is implemented so a cookie can be used as a comparator for a
     * SortedSet of cookies. Specifically it's used above in the
     * createCookieHeader method.
     * </p>
     *
     * @param o1
     *        The first object to be compared
     * @param o2
     *        The second object to be compared
     * @return See {@link java.util.Comparator#compare(Object,Object)}
     */
    @Override
    public int compare(final Cookie c1, final Cookie c2) {
        LOG.trace("enter Cookie.compare(Object, Object)");

        if (c1.getPath() == null && c2.getPath() == null) {
            return 0;
        } else if (c1.getPath() == null) {
            // null is assumed to be "/"
            if (c2.getPath().equals(CookieSpec.PATH_DELIM)) {
                return 0;
            } else {
                return -1;
            }
        } else if (c2.getPath() == null) {
            // null is assumed to be "/"
            if (c1.getPath().equals(CookieSpec.PATH_DELIM)) {
                return 0;
            } else {
                return 1;
            }
        } else {
            return c1.getPath().compareTo(c2.getPath());
        }
    }

    /**
     * Return a textual representation of the cookie.
     *
     * @return string.
     *
     * @see #toExternalForm
     */
    @Override
    public String toString() {
        return toExternalForm();
    }

    // ----------------------------------------------------- Instance Variables

    /** Comment attribute. */
    private String cookieComment;

    /** Domain attribute. */
    private String cookieDomain;

    /** Expiration {@link Date}. */
    private Date cookieExpiryDate;

    /** Path attribute. */
    private String cookiePath;

    /** My secure flag. */
    private boolean isSecure;

    /**
     * Specifies if the set-cookie header included a Path attribute for this
     * cookie
     */
    private boolean hasPathAttribute = false;

    /**
     * Specifies if the set-cookie header included a Domain attribute for this
     * cookie
     */
    private boolean hasDomainAttribute = false;

    /** The version of the cookie specification I was created from. */
    private int cookieVersion = 0;

    // -------------------------------------------------------------- Constants

    /** Log object for this class */
    private static final Log LOG = LogFactory.getLog(Cookie.class);

}
